/*
 * This file is part of freewm.
 *
 * freewm is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * freewm is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with freewm.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <dlfcn.h>

#include "freewm.h"
#include "incs.h"
#include "module.h"
#include "wm.h"

// Module linked list.
struct mod_link
{
	// Next and previous nodes.
	struct mod_link *next;
	struct mod_link *prev;
	
	char name[16], author[32], version[16];
	void *handle;
};

// Aliases linked list.
struct mod_alias_link
{
	// Next and previous nodes.
	struct mod_alias_link *next;
	struct mod_alias_link *prev;
	
	char name[16], alias[32], func[32];
};

// Module globals.
struct mod_link *mod_beg = NULL, *mod_end = NULL;
struct mod_alias_link *mod_event_beg = NULL, *mod_event_end = NULL;
struct mod_alias_link *mod_bind_beg = NULL, *mod_bind_end = NULL;

// Function prototypes.
struct mod_link *module_list_get(char *mod_name);

// Get address of symbol in module.  Basically a dlsym wrapper.
void *module_get_sym(void *handle, const char *sym_name)
{
	// Clear any existing errors.
	dlerror();
	
	void *location = dlsym(handle, sym_name);
	char *error;
	
	if ((error = dlerror()))
	{
		fprintf(stderr, "module_get_sym: problem finding %s: %s\n", sym_name, error);
		return NULL;
	}
	
	return location;
}

// Add a command bind.
void module_bind_add(const char *mod_name, const char *alias, const char *func)
{
	logger("module_bind_add: %s %s %s", mod_name, alias, func);
	
	// Build new link.
	struct mod_alias_link *lnk = (struct mod_alias_link *) malloc(sizeof(struct mod_alias_link));
	strncpy(lnk->name, mod_name, sizeof(lnk->name));
	strncpy(lnk->alias, alias, sizeof(lnk->alias));
	strncpy(lnk->func, func, sizeof(lnk->func));
	lnk->next = NULL;
	
	// Add link to end of list.
	if (mod_bind_end)
	{
		lnk->prev = mod_bind_end;
		mod_bind_end->next = lnk;
		mod_bind_end = lnk;
	}
	// First node!
	else
	{
		mod_bind_beg = lnk;
		mod_bind_end = lnk;
	}
}

// Delete a command bind.
void module_bind_del(const char *mod_name, const char *alias)
{
	logger("module_bind_del: %s %s", mod_name, alias);
	
	struct mod_alias_link *nxt, *cur = mod_bind_beg;
	while (cur)
	{
		nxt = NULL;
		
		// Found the alias to delete.
		if (!strcmp(cur->name, mod_name) && !strcmp(cur->alias, alias))
		{
			logger("Deleting bind: %s %s", mod_name, alias);
			
			if (cur->prev) cur->prev->next = cur->next;
			if (cur->next) cur->next->prev = cur->prev;
			if (cur == mod_bind_beg) mod_bind_beg = cur->next;
			if (cur == mod_bind_end) mod_bind_end = cur->prev;
			
			nxt = cur->next;
			
			// Free resources.
			free(cur);
			
			return;
		}
		
		// Go to next node.
		if (nxt)
			cur = nxt;
		else
			cur = cur->next;
	}
}

// Delete a command bind.
void module_bind_del_all(const char *mod_name)
{
	logger("module_bind_del_all: %s", mod_name);
	
	struct mod_alias_link *nxt, *cur = mod_bind_beg;
	while (cur)
	{
		nxt = NULL;
		
		// Found the alias to delete.
		if (!strcmp(cur->name, mod_name))
		{
			logger("Deleting bind: %s %s", mod_name, cur->alias);
			
			if (cur->prev) cur->prev->next = cur->next;
			if (cur->next) cur->next->prev = cur->prev;
			if (cur == mod_bind_beg) mod_bind_beg = cur->next;
			if (cur == mod_bind_end) mod_bind_end = cur->prev;
			
			nxt = cur->next;
			
			// Free resources.
			free(cur);
		}
		
		// Go to next node.
		if (nxt)
			cur = nxt;
		else
			cur = cur->next;
	}
}

// Execute a bind.
void module_bind_exec(const char *alias)
{
	typedef void (*m_func)(void);
	struct mod_link *mlnk;
	struct mod_alias_link *cur = mod_bind_beg;
	m_func f;
	
	while (cur)
	{
		// Found the alias to delete.
		if (!strcmp(cur->alias, alias))
		{
			logger("Executing bind: %s %s", cur->name, alias);
			mlnk = module_list_get(cur->name);
			
			if (mlnk)
			{
				f = (m_func) module_get_sym(mlnk->handle, cur->func);
				
				if (f) f();
				else logger("Could not execute %s in %s", cur->func, cur->name);
			}
		}
		
		// Go to next node.
		cur = cur->next;
	}
}

// Add an event bind.
void module_event_add(const char *mod_name, const char *alias, const char *func)
{
	logger("module_event_add: %s %s %s", mod_name, alias, func);
	
	// Build new link.
	struct mod_alias_link *lnk = (struct mod_alias_link *) malloc(sizeof(struct mod_alias_link));
	strncpy(lnk->name, mod_name, sizeof(lnk->name));
	strncpy(lnk->alias, alias, sizeof(lnk->alias));
	strncpy(lnk->func, func, sizeof(lnk->func));
	lnk->next = NULL;
	
	// Add link to end of list.
	if (mod_event_end)
	{
		lnk->prev = mod_event_end;
		mod_event_end->next = lnk;
		mod_event_end = lnk;
	}
	// First node!
	else
	{
		mod_event_beg = lnk;
		mod_event_end = lnk;
	}
}

// Delete an event bind.
void module_event_del(const char *mod_name, const char *alias)
{
	logger("module_event_del: %s %s", mod_name, alias);
	
	struct mod_alias_link *nxt, *cur = mod_event_beg;
	while (cur)
	{
		nxt = NULL;
		
		// Found the alias to delete.
		if (!strcmp(cur->name, mod_name) && !strcmp(cur->alias, alias))
		{
			logger("Deleting event bind: %s %s", mod_name, alias);
			
			if (cur->prev) cur->prev->next = cur->next;
			if (cur->next) cur->next->prev = cur->prev;
			if (cur == mod_event_beg) mod_event_beg = cur->next;
			if (cur == mod_event_end) mod_event_end = cur->prev;
			
			nxt = cur->next;
			
			// Free the resource.
			free(cur);
			
			return;
		}
		
		// Go to next node.
		if (nxt)
			cur = nxt;
		else
			cur = cur->next;
	}
}

// Delete an event bind.
void module_event_del_all(const char *mod_name)
{
	logger("module_event_del_all: %s", mod_name);
	
	struct mod_alias_link *nxt, *cur = mod_event_beg;
	while (cur)
	{
		nxt = NULL;
		
		// Found the alias to delete.
		if (!strcmp(cur->name, mod_name))
		{
			logger("Deleting event bind: %s %s", mod_name, cur->alias);
			
			if (cur->prev) cur->prev->next = cur->next;
			if (cur->next) cur->next->prev = cur->prev;
			if (cur == mod_event_beg) mod_event_beg = cur->next;
			if (cur == mod_event_end) mod_event_end = cur->prev;
			
			nxt = cur->next;
			
			// Free the resource.
			free(cur);
		}
		
		// Go to next node.
		if (nxt)
			cur = nxt;
		else
			cur = cur->next;
	}
}

// Execute an event bind.
void module_event_exec(const char *alias, XEvent *ev)
{
	logger("module_event_exec %s", alias);

	typedef void (*m_func)(XEvent *event);
	struct mod_link *mlnk;
	struct mod_alias_link *cur = mod_event_beg;
	m_func f;
	
	while (cur)
	{
		// Found the alias to delete.
		if (!strcmp(cur->alias, alias))
		{
			logger("Executing event bind: %s %s", cur->name, alias);
			mlnk = module_list_get(cur->name);
			
			if (mlnk)
			{
				f = (m_func) module_get_sym(mlnk->handle, cur->func);
				
				if (f) f(ev);
				else logger("Could not execute %s in %s", cur->func, cur->name);
			}
		}
		
		// Go to next node.
		cur = cur->next;
	}
}

// Add module to linked list.
void module_list_add(char *mname, char *mauthor, char *mversion, void *handle)
{	
	// Build new link.
	struct mod_link *lnk = (struct mod_link *) malloc(sizeof(struct mod_link));
	strncpy(lnk->name, mname, sizeof(lnk->name));
	strncpy(lnk->author, mauthor, sizeof(lnk->author));
	strncpy(lnk->version, mversion, sizeof(lnk->version));
	lnk->handle = handle;
	lnk->next = NULL;
	
	// Add link to end of list.
	if (mod_end)
	{
		lnk->prev = mod_end;
		mod_end->next = lnk;
		mod_end = lnk;
	}
	// First node!
	else
	{
		mod_beg = lnk;
		mod_end = lnk;
	}
}

// Retrieve a module link.
struct mod_link *module_list_get(char *mod_name)
{
	struct mod_link *cur = mod_beg;
	while (cur)
	{
		// Found the alias to delete.
		if (!strcmp(cur->name, mod_name))
		{
			// Return the requested module link.
			return cur;
		}
		
		// Go to next node.
		cur = cur->next;
	}
	
	return NULL;
}

// Load a freewm module.
int module_load(const char *path)
{
	logger("Loading module: %s...", path);
	
	void *handle;
	int *stay_loaded;
	char *mname, *mauthor, *mversion;
	void (*m_init)(void);
	void (*m_destroy)(void);
	
	// Try to open the module from the home directory.
	if (!(handle = dlopen(path, RTLD_LAZY)))
	{
		logger("Could not open %s: %s", path, dlerror());
		return 0;
	}
	
	// Retrieve all required module symbols.
	logger("Retrieving required symbols for %s...", path);
	mname = module_get_sym(handle, "module_name");
	mauthor = module_get_sym(handle, "module_author");
	mversion = module_get_sym(handle, "module_version");
	stay_loaded = module_get_sym(handle, "stay_loaded");
	m_init = module_get_sym(handle, "module_init");
	m_destroy = module_get_sym(handle, "module_destroy");
	
	// Make sure the module met requirements.
	if (!(mname || mauthor || mversion || stay_loaded || m_init || m_destroy))
	{
		fprintf(stderr, "Module %s did not have required symbols!\n", path);
		dlclose(handle);
		return 0;
	}
	
	// Run module_init()
	logger("module_init() for %s started...", path);
	XGrabServer(dpy);
	m_init();
	XSync(dpy, 0);
	XUngrabServer(dpy);
	logger("module_init() for %s complete.", path);
	
	// Add to linked list.
	logger("Adding %s to module list...", mname);
	module_list_add(mname, mauthor, mversion, handle);
	
	// Success.
	return 1;
}

// Load all freewm modules.
void module_load_all(const char *path)
{
	DIR *d;
	struct dirent *dir;
	char full_path[MAXPATHLEN];
	
	// Open the directory.
	d = opendir(path);
	if (d)
	{
		while ((dir = readdir(d)))
		{
			if (!strcmp(dir->d_name, ".")) continue; 
			if (!strcmp(dir->d_name, "..")) continue;
			if (dir->d_type == DT_DIR) continue;
			
			snprintf(full_path, sizeof(full_path), "%s%s", path, dir->d_name);
			module_load(full_path);
		}
		
		closedir(d);
	}
}

// Unload a freewm module.
void module_unload(const char *mod_name)
{
	logger("Unloading module...");
	
	struct mod_link *cur = mod_beg;
	void (*m_destroy)(void);
	
	// Remove link from the modules list.
	while (cur)
	{
		// Found the alias to delete.
		if (!strcmp(cur->name, mod_name))
		{
			logger("Running module_destroy on %s", mod_name);
			m_destroy = module_get_sym(cur->handle, "module_destroy");
			if (m_destroy) m_destroy();
			else logger("Could not execute mod_destroy on %s", mod_name);
			
			logger("Closing module %s handle...", mod_name);
			dlclose(cur->handle);
			
			logger("Deleting mod node %s", mod_name);
			
			if (cur->prev) cur->prev->next = cur->next;
			if (cur->next) cur->next->prev = cur->prev;
			if (cur == mod_beg) mod_beg = cur->next;
			if (cur == mod_end) mod_end = cur->prev;
			
			// Free resources.
			free(cur);
			
			// Success.
			break;
		}
		
		// Go to next node.
		cur = cur->next;
	}
	
	// Remove all associated events and binds.
	module_bind_del_all(mod_name);
	module_event_del_all(mod_name);
}

// Unload all freewm modules.
void module_unload_all(void)
{
	logger("Unloading all modules...");
	
	struct mod_link *nxt, *cur = mod_beg;
	typedef void (*m_destroy)(void);
	m_destroy f;
	
	// Remove link from the modules list.
	while (cur)
	{	
		logger("Running module_destroy on %s", cur->name);
		f = (m_destroy) module_get_sym(cur->handle, "module_destroy");
		if (f) f();
		else logger("Could not execute mod_destroy on %s", cur->name);
		
		logger("Closing module %s handle...", cur->name);
		dlclose(cur->handle);
		
		logger("Deleting mod node %s", cur->name);
		
		if (cur->prev) cur->prev->next = cur->next;
		if (cur->next) cur->next->prev = cur->prev;
		if (cur == mod_beg) mod_beg = cur->next;
		if (cur == mod_end) mod_end = cur->prev;
		
		// Remove all associated events and binds.
		module_bind_del_all(cur->name);
		module_event_del_all(cur->name);
		
		nxt = cur->next;
		
		// Free resources.
		free(cur);
		
		// Go to next node.
		cur = nxt;
	}
}

// Reload a freewm module.
void module_reload(const char *mod_name)
{
	logger("Reloading module...");
	
	module_unload(mod_name);
	module_load(mod_name);
}

// Reload all freewm modules.
void module_reload_all(void)
{
	logger("Reloading all modules...");
	module_unload_all();
	freewm_load_modules();
}
